package utils.query;

import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import play.db.jpa.JPA;
import play.db.jpa.Model;

import javax.persistence.EntityManager;
import javax.persistence.Query;
import java.sql.Date;
import java.util.*;

public class ModelQuery<T extends Model> {

	private static Logger logger = LoggerFactory.getLogger(ModelQuery.class);

	protected Class<T> entityClass;
	protected static String alias = "t";
	protected Map<String, Object> params;
	protected List<OrderBy> orderBys = new ArrayList<OrderBy>();
	protected Map<String, Class> fieldTypes = new HashMap<String, Class>();
	protected Map<String, Object> conditionValues = new HashMap<String, Object>();
	protected List<String> conditionContents = new ArrayList<String>();
	private Query query;
	private Query countQuery;
	private List<String> filterContents = new ArrayList<String>();

	@SuppressWarnings("unused")
	private Map<String, Object> filterValues = new HashMap<String, Object>();
	
	private List<String> joins = new ArrayList<String>(); 
	
	private boolean builded = false;
	
	private String selectString = "t"; 
	
	protected ModelQuery() {
	}

	/**
	 * 构建一个查询
	 * 
	 * @param <E>
	 * @param entityClass
	 * @return
	 */
	public static <E extends Model> ModelQuery<E> query(
			final Class<E> entityClass) {
		return query(entityClass, null);
	}

	/**
	 * 构建一个查询,可带查询条件params
	 * params 格式参考如下<br>
	 * {"t.name":["名称1"],<br>
	 *  "t.age": ["28"],<br>
	 *  "t.whatever":["foo","bar"]}
	 * 
	 * @param <E>
	 * @param entityClass
	 * @return
	 */
	public static <E extends Model> ModelQuery<E> query(
			final Class<E> entityClass, Map<String, String[]> params) {
		ModelQuery<E> query = new ModelQuery<E>();
		query.entityClass = entityClass;
		query.fieldTypes.putAll(FieldType.buildFieldTypes(entityClass, alias,
				new HashSet<Class>()));
		if (params != null && params.size() > 0) {
			query.where(params);
		}
		return query;
	}
	
	public ModelQuery<T> innerJoin(String join, String alias) {
		joins.add("inner join "+join+ " as "+ alias); 
		return this; 
	}
	public ModelQuery<T> leftJoin(String join, String alias) {
		joins.add("left join "+join+ " as "+ alias); 
		return this; 
	}
	public ModelQuery<T> rightJoin(String join, String alias) {
		joins.add("right join "+join+ " as "+ alias); 
		return this; 
	}
	
	public ModelQuery<T> select(String select) {
		this.selectString = select; 
		return this; 
	}

	public void addFilterCotents(String value){
		this.filterContents.add(value);
	}

	/**
	 * 增加额外的条件，如下例所示 where("t.age=:age ","20");
	 * @param extraCondition
	 * @param value
	 * @return
	 */
	public ModelQuery<T> where(String extraCondition, String[] value) {
		String key = StringUtils.substringBetween(extraCondition, ":", " ");
		String replacement = "extra_" + StringUtils.replace(key,".","_");
		String replace = StringUtils.replace(extraCondition, ":" + key, ":"
				+ replacement);

		Class fieldClass = fieldTypes.get("t." + key);
		conditionContents.add(replace);
		if (value.length == 1){
			if(Enum.class.isAssignableFrom(fieldClass)){
				conditionValues.put(replacement,Enum.valueOf(fieldClass, value[0]));
			}else { 
				conditionValues.put(replacement,
					ConvertUtils.convert(value[0], fieldClass));
			}
		}else if(value.length>1){
			List list = new ArrayList();
			for (String v : value) {
				list.add(ConvertUtils.convert(v, fieldClass));
			}
			conditionValues.put(replacement, list);
		}
		needRebuild();
		return this;
	}

	private ModelQuery<T> where(Map<String, String[]> params) {
		for (String s : params.keySet()) {
			String[] values = params.get(s);
			if (values.length == 1
					&& StringUtils.isEmpty(StringUtils.trim(values[0]))) {
				continue;
			} else {
				addCondition(s, values);
			}
		}
		needRebuild();
		return this;
	}

	private void needRebuild() {
		builded = false;
	}

	private void addCondition(String s, String[] values) {
		Class fieldClass = fieldTypes.get(s);
		if (fieldClass == null || allEmpty(values)) return; 
		String[] newValues = trimToNew(values); 
		String key = StringUtils.replace(s, ".", "_");

		if (Date.class.isAssignableFrom(fieldClass)) {
			if (newValues.length == 2 && allNotEmpty(newValues)) {
				conditionContents.add(s + " between :" + key + "_begin and :"
						+ key + "_end");
				conditionValues.put(key + "_begin",
						ConvertUtils.convert(newValues[0], Date.class));
				conditionValues.put(key + "_end",
						ConvertUtils.convert(newValues[1], Date.class));
			} else if (newValues.length == 2) {
				String value = StringUtils.isNotEmpty(newValues[0]) ? newValues[0]
						: newValues[1];
				conditionContents.add(s + " =:" + key);
				conditionValues.put(key,
						ConvertUtils.convert(value, Date.class));
			}
		} else if (String.class.isAssignableFrom(fieldClass)) {
			if(newValues.length==1){
				conditionContents.add("lower(" + s + ") like :" + key);
				conditionValues.put(key, "%"
						+ StringUtils.trim(newValues[0]).toLowerCase() + "%");
			}else if(newValues.length>1) { 
				conditionContents.add(s + " in :" + key);
				List list = new ArrayList(); 
				for(String value : newValues) { 
					list.add(ConvertUtils.convert(
							StringUtils.trim(value), fieldClass));
				}
				conditionValues.put(key, list);
			}
		} else if(Enum.class.isAssignableFrom(fieldClass)){
			conditionContents.add(s + " =:" + key);
			conditionValues.put(key,Enum.valueOf(fieldClass, newValues[0]));
		}else if(Number.class.isAssignableFrom(fieldClass)){ 
			if (newValues.length == 2 && allNotEmpty(newValues)) {
				conditionContents.add(s + " between :" + key + "_begin and :"
						+ key + "_end");
				conditionValues.put(key + "_begin",
						ConvertUtils.convert(newValues[0], fieldClass));
				conditionValues.put(key + "_end",
						ConvertUtils.convert(newValues[1], fieldClass));
			}else { 
				String value = StringUtils.isNotEmpty(newValues[0]) ? newValues[0]
						: "0";
				conditionContents.add(s + " =:" + key);
				conditionValues.put(key,
						ConvertUtils.convert(value, fieldClass));
			}
		}else{
			if(newValues.length==1){
				conditionContents.add(s + "=:" + key);
				conditionValues.put(key, ConvertUtils.convert(
						StringUtils.trim(newValues[0]), fieldClass));
			}else if(newValues.length>1) { 
				conditionContents.add(s + " in :" + key);
				List list = new ArrayList(); 
				for(String value : newValues) { 
					list.add(ConvertUtils.convert(
							StringUtils.trim(value), fieldClass));
				}
				conditionValues.put(key, list);
			}
		}
	}

	private String[] trimToNew(String[] values) {
		List<String> list = new ArrayList<String>(); 
		for (String string : values) {
			if(StringUtils.isNotEmpty(string)) { 
				list.add(string); 
			}
		}
		return list.toArray(new String[0]);
	}

	private boolean allNotEmpty(String[] values) {
		boolean flag = true;
		for (String string : values) {
			if (StringUtils.isEmpty(StringUtils.trim(string))) {
				return false;
			}
		}
		return flag;
	}

	private boolean allEmpty(String[] values) {
		for (String string : values) {
			if (StringUtils.isNotEmpty(StringUtils.trim(string))) {
				return false;
			}
		}
		return true;
	}

	public ModelQuery<T> orderBy(OrderBy by) {
		this.orderBys.add(by);
		return this;
	}

	public ModelQuery<T> orderBy(String by) {
		String[] bys = StringUtils.split(by);
		OrderBy orderBy = new OrderBy(bys[0],bys[1].equals("asc")? OrderBy.OrderType.asc: OrderBy.OrderType.desc);
		this.orderBys.add(orderBy);
		return this;
	}

	public ModelQuery<T> clearOrderBy() {
		this.orderBys.clear();
		return this;
	}
	public ModelQuery<T> orderBy(Collection<OrderBy> orderBys) {
		this.orderBys.addAll(orderBys);
		return this;
	}
	
	public List<OrderBy> getOrderBys() {
		return this.orderBys; 
	}

	private ModelQuery<T> build() {
		EntityManager em = JPA.em();
		query = em.createQuery(this.genQueryString());
		countQuery = em.createQuery(this.genCountString());

		logger.debug("query: {}", query.toString());
		logger.debug("countQuery: {}", countQuery.toString());

		for (Iterator iterator = conditionValues.keySet().iterator(); 
				iterator.hasNext();) {
			String name = (String) iterator.next();
			query.setParameter(name, conditionValues.get(name));
			countQuery.setParameter(name, conditionValues.get(name));
		}
		play.Logger.debug(this.genQueryString());
		return this;
	}

	/**
	 * 
	 * @return
	 */
	public int count() {
		initBuild();
		return NumberUtils.toInt(this.countQuery.getSingleResult().toString());
	}

	private void initBuild() {
		if (!builded) {
			build();
			builded = true;
		}
	}

	/**
	 * 
	 * @return
	 */
	public List<T> getResults(int from, int max) {
		if (from < 0 || max <= 0)
			return Collections.EMPTY_LIST;
		initBuild();
		return query.setFirstResult(from).setMaxResults(max).getResultList();
	}
	
	public List<T> getResults() {
		initBuild();
		return query.getResultList();
	}


	private void addFilters(StringBuffer buffer) {
		
		for (String value : filterContents) {
			buffer.append(" and "+ value);
		}
	}
	
	private void addOrders(StringBuffer buffer) {
		if (orderBys.size() == 0) { 
			buffer.append(" order by t.id desc");
			return; 
		}
		
		buffer.append(" order by ");
		int count = 0;
		for (OrderBy orderBy : orderBys) {
			if (count > 0) {
				buffer.append(",");
			}
			buffer.append(orderBy.toString());
			count++;
		}
	}
	
	private void addConditions(StringBuffer buffer) {
		buffer.append(" where 1=1 ");
		for (String content : conditionContents) {
			buffer.append(" and " + content);
		}
	}
	
	private String genQueryString() {
		StringBuffer buffer = new StringBuffer("select " + selectString + " from "
				+ this.entityClass.getName() + " " + this.alias);
		addJoin(buffer); 
		addConditions(buffer);
		addFilters(buffer);
		addOrders(buffer);
		return buffer.toString();
	}


	private String genCountString() {
		StringBuffer buffer = new StringBuffer("select count("+selectString+") from "
				+ this.entityClass.getName() + " " + this.alias);
		addJoin(buffer); 
		addConditions(buffer);
		addFilters(buffer);
		return buffer.toString();
	}

	private void addJoin(StringBuffer buffer) {
		buffer.append(" ");
		for (String join : joins) {
			buffer.append(join); 
		}
	}


}
